/*
 * Copyright 2009 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.js;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.js.JavaRef;
import com.google.gwt.core.ext.js.JavaScriptAst;
import com.google.gwt.core.ext.js.JsniFunction;
import com.google.gwt.core.ext.js.JsniFunctionParseContext;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.impl.StandardCompilationAnalysis;
import com.google.gwt.core.ext.soyc.Range;
import com.google.gwt.dev.PermutationResult;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.JJSOptions;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.impl.CodeSplitter;
import com.google.gwt.dev.jjs.impl.GenerateJavaScriptAST;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.js.JsReportGenerationVisitor.CountingTextOutput;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.util.DefaultTextOutput;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Default implementation of {@link JavaScriptAst}.
 */
public class JavaScriptAstImpl extends JavaScriptAst {

  private static class PermutationResultImpl implements PermutationResult {
    private final ArtifactSet artifacts = new ArtifactSet();
    private final String[] js;
    private final SortedMap<String, String> symbolMap;

    public PermutationResultImpl(String[] js,
        SortedMap<String, String> symbolMap) {
      this.js = js;
      this.symbolMap = symbolMap;
    }

    public ArtifactSet getArtifacts() {
      return artifacts;
    }

    public String[] getJs() {
      return js;
    }

    public SortedMap<String, String> getSymbolMap() {
      return symbolMap;
    }
  }

  private static JavaToJavaScriptMap addStringLiteralMap(
      final JavaToJavaScriptMap map, final Map<JsName, String> stringLiteralMap) {
    JavaToJavaScriptMap postStringInterningMap = new JavaToJavaScriptMap() {
      public JsName nameForMethod(JMethod method) {
        return map.nameForMethod(method);
      }

      public JsName nameForType(JReferenceType type) {
        return map.nameForType(type);
      }

      public JField nameToField(JsName name) {
        return map.nameToField(name);
      }

      public JMethod nameToMethod(JsName name) {
        return map.nameToMethod(name);
      }

      public String stringLiteralForName(JsName name) {
        return stringLiteralMap.get(name);
      }

      public JReferenceType typeForStatement(JsStatement stat) {
        return map.typeForStatement(stat);
      }

      public JMethod vtableInitToMethod(JsStatement stat) {
        return map.vtableInitToMethod(stat);
      }
    };
    return postStringInterningMap;
  }

  private static SortedMap<String, String> makeSymbolMap(
      Map<String, JsName> symbolTable) {

    SortedMap<String, String> toReturn = new TreeMap<String, String>();

    for (Map.Entry<String, JsName> entry : symbolTable.entrySet()) {
      toReturn.put(entry.getKey(), entry.getValue().getShortIdent());
    }

    return toReturn;
  }

  private final Map<JsNameRef, JavaRef> jsniMap = new IdentityHashMap<JsNameRef, JavaRef>();

  private final JsProgram jsProgram;

  private JJSOptions options;

  public JavaScriptAstImpl(JJSOptions options) {
    this.jsProgram = new JsProgram(options.isSoycEnabled());
    this.options = options;
  }

  @Override
  public PermutationResult generateScript(TreeLogger logger, JProgram jprogram)
      throws InterruptedException {
    Map<String, JsName> symbolTable = new HashMap<String, JsName>();
    JavaToJavaScriptMap map = GenerateJavaScriptAST.exec(jprogram, jsProgram,
        jsniMap, options.getOutput(), symbolTable);

    // (8) Normalize the JS AST.
    // Fix invalid constructs created during JS AST gen.
    JsNormalizer.exec(jsProgram);
    // Resolve all unresolved JsNameRefs.
    JsSymbolResolver.exec(jsProgram);
    // Move all function definitions to a top-level scope, to reduce weirdness
    EvalFunctionsAtTopScope.exec(jsProgram);

    // (9) Optimize the JS AST.
    if (options.isAggressivelyOptimize()) {
      boolean didChange;
      do {
        if (Thread.interrupted()) {
          throw new InterruptedException();
        }

        didChange = false;
        // Remove unused functions, possible
        didChange = JsStaticEval.exec(jsProgram) || didChange;
        // Inline JavaScript function invocations
        didChange = JsInliner.exec(jsProgram) || didChange;
        // Remove unused functions, possible
        didChange = JsUnusedFunctionRemover.exec(jsProgram) || didChange;
      } while (didChange);
    }

    // (10) Obfuscate
    final Map<JsName, String> stringLiteralMap;
    switch (options.getOutput()) {
      case OBFUSCATED:
        stringLiteralMap = JsStringInterner.exec(jsProgram);
        JsObfuscateNamer.exec(jsProgram);
        break;
      case PRETTY:
        // We don't intern strings in pretty mode to improve readability
        stringLiteralMap = new HashMap<JsName, String>();
        JsPrettyNamer.exec(jsProgram);
        break;
      case DETAILED:
        stringLiteralMap = JsStringInterner.exec(jsProgram);
        JsVerboseNamer.exec(jsProgram);
        break;
      default:
        throw new InternalCompilerException("Unknown output mode");
    }

    JavaToJavaScriptMap postStringInterningMap = addStringLiteralMap(map,
        stringLiteralMap);

    // (10.5) Split up the program into fragments
    if (options.isAggressivelyOptimize() && options.isRunAsyncEnabled()) {
      CodeSplitter.exec(logger, jprogram, jsProgram, postStringInterningMap);
    }

    // (11) Perform any post-obfuscation normalizations.

    // Work around an IE7 bug,
    // http://code.google.com/p/google-web-toolkit/issues/detail?id=1440
    JsIEBlockSizeVisitor.exec(jsProgram);

    // (12) Generate the final output text.
    String[] js = new String[jsProgram.getFragmentCount()];
    List<Map<Range, SourceInfo>> sourceInfoMaps = options.isSoycEnabled()
        ? new ArrayList<Map<Range, SourceInfo>>(jsProgram.getFragmentCount())
        : null;

    for (int i = 0; i < js.length; i++) {
      if (sourceInfoMaps != null) {
        CountingTextOutput out = new CountingTextOutput(
            options.getOutput().shouldMinimize());
        JsReportGenerationVisitor v = new JsReportGenerationVisitor(out);
        v.accept(jsProgram.getFragmentBlock(i));
        js[i] = out.toString();
        sourceInfoMaps.add(v.getSourceInfoMap());
      } else {
        DefaultTextOutput out = new DefaultTextOutput(
            options.getOutput().shouldMinimize());
        JsSourceGenerationVisitor v = new JsSourceGenerationVisitor(out);
        v.accept(jsProgram.getFragmentBlock(i));
        js[i] = out.toString();
      }
    }

    SortedMap<String, String> symbolMap = makeSymbolMap(symbolTable);

    PermutationResult toReturn = new PermutationResultImpl(js, symbolMap);
    if (sourceInfoMaps != null) {
      toReturn.getArtifacts().add(
          new StandardCompilationAnalysis(logger, sourceInfoMaps,
              jprogram.getSplitPointMap()));
    }

    return toReturn;
  }

  @Override
  public List<JavaRef> getExternalJavaRefs() {
    // TODO Auto-generated method stub
    return Collections.emptyList();
  }

  @Override
  public JsniFunction parseJsniFunction(JsniFunctionParseContext context,
      JMethod method, String js) throws IllegalArgumentException,
      IllegalStateException {
    return JsniFunctionImpl.parse(context, method, js, jsProgram, jsniMap);
  }
}
